package jinyilw.swing.rlaf.ui;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.JTextField;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
import javax.swing.UIDefaults.LazyValue;
import javax.swing.UIManager;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.Keymap;
import javax.swing.text.TextAction;
import javax.swing.text.Utilities;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;

public class AquaKeyBindings
{
	private static volatile AquaKeyBindings singleton;

	public static AquaKeyBindings getAquaKeyBindings()
	{
		if (singleton == null)
			synchronized (AquaKeyBindings.class)
			{
				if (singleton == null)
					singleton = new AquaKeyBindings();
			}
		return singleton;
	}

	private AquaKeyBindings()
	{
	}

	final DefaultKeyTypedAction defaultKeyTypedAction = new DefaultKeyTypedAction();
	void setDefaultAction(final String keymapName)
	{
		final Keymap map = JTextComponent.getKeymap(keymapName);
		map.setDefaultAction(defaultKeyTypedAction);
	}

	static final String upMultilineAction = "aqua-move-up";
	static final String downMultilineAction = "aqua-move-down";
	static final String pageUpMultiline = "aqua-page-up";
	static final String pageDownMultiline = "aqua-page-down";

	final String[] commonTextEditorBindings = {"ENTER", JTextField.notifyAction,
			"COPY", DefaultEditorKit.copyAction, "CUT",
			DefaultEditorKit.cutAction, "PASTE", DefaultEditorKit.pasteAction,
			"meta A", DefaultEditorKit.selectAllAction, "meta C",
			DefaultEditorKit.copyAction, "meta V", DefaultEditorKit.pasteAction,
			"meta X", DefaultEditorKit.cutAction, "meta BACK_SLASH", "unselect",

			"DELETE", DefaultEditorKit.deleteNextCharAction, "alt DELETE",
			"delete-next-word", "BACK_SPACE",
			DefaultEditorKit.deletePrevCharAction, "shift BACK_SPACE",
			DefaultEditorKit.deletePrevCharAction, "alt BACK_SPACE",
			"delete-previous-word",

			"LEFT", DefaultEditorKit.backwardAction, "KP_LEFT",
			DefaultEditorKit.backwardAction, "RIGHT",
			DefaultEditorKit.forwardAction, "KP_RIGHT",
			DefaultEditorKit.forwardAction, "shift LEFT",
			DefaultEditorKit.selectionBackwardAction, "shift KP_LEFT",
			DefaultEditorKit.selectionBackwardAction, "shift RIGHT",
			DefaultEditorKit.selectionForwardAction, "shift KP_RIGHT",
			DefaultEditorKit.selectionForwardAction, "meta LEFT",
			DefaultEditorKit.beginLineAction, "meta KP_LEFT",
			DefaultEditorKit.beginLineAction, "meta RIGHT",
			DefaultEditorKit.endLineAction, "meta KP_RIGHT",
			DefaultEditorKit.endLineAction, "shift meta LEFT",
			DefaultEditorKit.selectionBeginLineAction, "shift meta KP_LEFT",
			DefaultEditorKit.selectionBeginLineAction, "shift meta RIGHT",
			DefaultEditorKit.selectionEndLineAction, "shift meta KP_RIGHT",
			DefaultEditorKit.selectionEndLineAction, "alt LEFT",
			DefaultEditorKit.previousWordAction, "alt KP_LEFT",
			DefaultEditorKit.previousWordAction, "alt RIGHT",
			DefaultEditorKit.nextWordAction, "alt KP_RIGHT",
			DefaultEditorKit.nextWordAction, "shift alt LEFT",
			DefaultEditorKit.selectionPreviousWordAction, "shift alt KP_LEFT",
			DefaultEditorKit.selectionPreviousWordAction, "shift alt RIGHT",
			DefaultEditorKit.selectionNextWordAction, "shift alt KP_RIGHT",
			DefaultEditorKit.selectionNextWordAction,

			"control A", DefaultEditorKit.beginLineAction, "control B",
			DefaultEditorKit.backwardAction, "control D",
			DefaultEditorKit.deleteNextCharAction, "control E",
			DefaultEditorKit.endLineAction, "control F",
			DefaultEditorKit.forwardAction, "control H",
			DefaultEditorKit.deletePrevCharAction, "control W",
			"delete-previous-word", "control shift O",
			"toggle-componentOrientation",

			"END", DefaultEditorKit.endAction, "HOME",
			DefaultEditorKit.beginAction, "shift END",
			DefaultEditorKit.selectionEndAction, "shift HOME",
			DefaultEditorKit.selectionBeginAction,

			"PAGE_DOWN", pageDownMultiline, "PAGE_UP", pageUpMultiline,
			"shift PAGE_DOWN", "selection-page-down", "shift PAGE_UP",
			"selection-page-up", "meta shift PAGE_DOWN", "selection-page-right",
			"meta shift PAGE_UP", "selection-page-left",

			"meta DOWN", DefaultEditorKit.endAction, "meta KP_DOWN",
			DefaultEditorKit.endAction, "meta UP", DefaultEditorKit.beginAction,
			"meta KP_UP", DefaultEditorKit.beginAction, "shift meta DOWN",
			DefaultEditorKit.selectionEndAction, "shift meta KP_DOWN",
			DefaultEditorKit.selectionEndAction, "shift meta UP",
			DefaultEditorKit.selectionBeginAction, "shift meta KP_UP",
			DefaultEditorKit.selectionBeginAction,};

	LateBoundInputMap getTextFieldInputMap()
	{
		return new LateBoundInputMap(
				new SimpleBinding(commonTextEditorBindings),
				new SimpleBinding(new String[]{"DOWN",
						DefaultEditorKit.endLineAction, "KP_DOWN",
						DefaultEditorKit.endLineAction, "UP",
						DefaultEditorKit.beginLineAction, "KP_UP",
						DefaultEditorKit.beginLineAction, "shift DOWN",
						DefaultEditorKit.selectionEndLineAction,
						"shift KP_DOWN",
						DefaultEditorKit.selectionEndLineAction, "shift UP",
						DefaultEditorKit.selectionBeginLineAction,
						"shift KP_UP",
						DefaultEditorKit.selectionBeginLineAction,

						"control P", DefaultEditorKit.beginAction, "control N",
						DefaultEditorKit.endAction, "control V",
						DefaultEditorKit.endAction,}));
	}

	LateBoundInputMap getPasswordFieldInputMap()
	{
		return new LateBoundInputMap(
				new SimpleBinding(getTextFieldInputMap().getBindings()),
				new SimpleBinding(new String[]{"alt LEFT", null, "alt KP_LEFT",
						null, "alt RIGHT", null, "alt KP_RIGHT", null,
						"shift alt LEFT", null, "shift alt KP_LEFT", null,
						"shift alt RIGHT", null, "shift alt KP_RIGHT", null,}));
	}

	LateBoundInputMap getMultiLineTextInputMap()
	{
		return new LateBoundInputMap(
				new SimpleBinding(commonTextEditorBindings),
				new SimpleBinding(new String[]{"ENTER",
						DefaultEditorKit.insertBreakAction, "DOWN",
						downMultilineAction, "KP_DOWN", downMultilineAction,
						"UP", upMultilineAction, "KP_UP", upMultilineAction,
						"shift DOWN", DefaultEditorKit.selectionDownAction,
						"shift KP_DOWN", DefaultEditorKit.selectionDownAction,
						"shift UP", DefaultEditorKit.selectionUpAction,
						"shift KP_UP", DefaultEditorKit.selectionUpAction,
						"alt shift DOWN",
						DefaultEditorKit.selectionEndParagraphAction,
						"alt shift KP_DOWN",
						DefaultEditorKit.selectionEndParagraphAction,
						"alt shift UP",
						DefaultEditorKit.selectionBeginParagraphAction,
						"alt shift KP_UP",
						DefaultEditorKit.selectionBeginParagraphAction,

						"control P", DefaultEditorKit.upAction, "control N",
						DefaultEditorKit.downAction, "control V",
						pageDownMultiline,

						"TAB", DefaultEditorKit.insertTabAction, "meta SPACE",
						"activate-link-action", "meta T", "next-link-action",
						"meta shift T", "previous-link-action",

						"END", DefaultEditorKit.endAction, "HOME",
						DefaultEditorKit.beginAction, "shift END",
						DefaultEditorKit.selectionEndAction, "shift HOME",
						DefaultEditorKit.selectionBeginAction,

						"PAGE_DOWN", pageDownMultiline, "PAGE_UP",
						pageUpMultiline, "shift PAGE_DOWN",
						"selection-page-down", "shift PAGE_UP",
						"selection-page-up", "meta shift PAGE_DOWN",
						"selection-page-right", "meta shift PAGE_UP",
						"selection-page-left",}));
	}

	LateBoundInputMap getFormattedTextFieldInputMap()
	{
		return new LateBoundInputMap(getTextFieldInputMap(),
				new SimpleBinding(
						new String[]{"UP", "increment", "KP_UP", "increment",
								"DOWN", "decrement", "KP_DOWN", "decrement",

								"ESCAPE", "reset-field-edit",}));
	}

	LateBoundInputMap getComboBoxInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(
				new String[]{"ESCAPE", "aquaHidePopup", "PAGE_UP",
						"aquaSelectPageUp", "PAGE_DOWN", "aquaSelectPageDown",
						"HOME", "aquaSelectHome", "END", "aquaSelectEnd",
						"ENTER", "enterPressed", "UP", "aquaSelectPrevious",
						"KP_UP", "aquaSelectPrevious", "DOWN", "aquaSelectNext",
						"KP_DOWN", "aquaSelectNext", "SPACE", "aquaSpacePressed" // "spacePopup"
				}));
	}

	LateBoundInputMap getListInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"meta C",
				"copy", "meta V", "paste", "meta X", "cut", "COPY", "copy",
				"PASTE", "paste", "CUT", "cut", "UP", "selectPreviousRow",
				"KP_UP", "selectPreviousRow", "shift UP",
				"selectPreviousRowExtendSelection", "shift KP_UP",
				"selectPreviousRowExtendSelection", "DOWN", "selectNextRow",
				"KP_DOWN", "selectNextRow", "shift DOWN",
				"selectNextRowExtendSelection", "shift KP_DOWN",
				"selectNextRowExtendSelection", "LEFT", "selectPreviousColumn",
				"KP_LEFT", "selectPreviousColumn", "shift LEFT",
				"selectPreviousColumnExtendSelection", "shift KP_LEFT",
				"selectPreviousColumnExtendSelection", "RIGHT",
				"selectNextColumn", "KP_RIGHT", "selectNextColumn",
				"shift RIGHT", "selectNextColumnExtendSelection",
				"shift KP_RIGHT", "selectNextColumnExtendSelection", "meta A",
				"selectAll",
				"HOME", "aquaHome", "shift HOME",
				"selectFirstRowExtendSelection", "END", "aquaEnd", "shift END",
				"selectLastRowExtendSelection",
				"shift PAGE_UP", "scrollUpExtendSelection", "shift PAGE_DOWN",
				"scrollDownExtendSelection"}));
	}

	LateBoundInputMap getScrollBarInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"RIGHT",
				"positiveUnitIncrement", "KP_RIGHT", "positiveUnitIncrement",
				"DOWN", "positiveUnitIncrement", "KP_DOWN",
				"positiveUnitIncrement", "PAGE_DOWN", "positiveBlockIncrement",
				"LEFT", "negativeUnitIncrement", "KP_LEFT",
				"negativeUnitIncrement", "UP", "negativeUnitIncrement", "KP_UP",
				"negativeUnitIncrement", "PAGE_UP", "negativeBlockIncrement",
				"HOME", "minScroll", "END", "maxScroll"}));
	}

	LateBoundInputMap getScrollBarRightToLeftInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"RIGHT",
				"negativeUnitIncrement", "KP_RIGHT", "negativeUnitIncrement",
				"LEFT", "positiveUnitIncrement", "KP_LEFT",
				"positiveUnitIncrement"}));
	}

	LateBoundInputMap getScrollPaneInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"RIGHT",
				"unitScrollRight", "KP_RIGHT", "unitScrollRight", "DOWN",
				"unitScrollDown", "KP_DOWN", "unitScrollDown", "LEFT",
				"unitScrollLeft", "KP_LEFT", "unitScrollLeft", "UP",
				"unitScrollUp", "KP_UP", "unitScrollUp", "PAGE_UP", "scrollUp",
				"PAGE_DOWN", "scrollDown", "HOME", "scrollHome", "END",
				"scrollEnd"}));
	}

	LateBoundInputMap getSliderInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"RIGHT",
				"positiveUnitIncrement", "KP_RIGHT", "positiveUnitIncrement",
				"DOWN", "negativeUnitIncrement", "KP_DOWN",
				"negativeUnitIncrement", "PAGE_DOWN", "negativeBlockIncrement",
				"LEFT", "negativeUnitIncrement", "KP_LEFT",
				"negativeUnitIncrement", "UP", "positiveUnitIncrement", "KP_UP",
				"positiveUnitIncrement", "PAGE_UP", "positiveBlockIncrement",
				"HOME", "minScroll", "END", "maxScroll"}));
	}

	LateBoundInputMap getSliderRightToLeftInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"RIGHT",
				"negativeUnitIncrement", "KP_RIGHT", "negativeUnitIncrement",
				"LEFT", "positiveUnitIncrement", "KP_LEFT",
				"positiveUnitIncrement"}));
	}

	LateBoundInputMap getSpinnerInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(
				new String[]{"UP", "increment", "KP_UP", "increment", "DOWN",
						"decrement", "KP_DOWN", "decrement"}));
	}

	LateBoundInputMap getTableInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"meta C",
				"copy", "meta V", "paste", "meta X", "cut", "COPY", "copy",
				"PASTE", "paste", "CUT", "cut", "RIGHT", "selectNextColumn",
				"KP_RIGHT", "selectNextColumn", "LEFT", "selectPreviousColumn",
				"KP_LEFT", "selectPreviousColumn", "DOWN", "selectNextRow",
				"KP_DOWN", "selectNextRow", "UP", "selectPreviousRow", "KP_UP",
				"selectPreviousRow", "shift RIGHT",
				"selectNextColumnExtendSelection", "shift KP_RIGHT",
				"selectNextColumnExtendSelection", "shift LEFT",
				"selectPreviousColumnExtendSelection", "shift KP_LEFT",
				"selectPreviousColumnExtendSelection", "shift DOWN",
				"selectNextRowExtendSelection", "shift KP_DOWN",
				"selectNextRowExtendSelection", "shift UP",
				"selectPreviousRowExtendSelection", "shift KP_UP",
				"selectPreviousRowExtendSelection", "PAGE_UP",
				"scrollUpChangeSelection", "PAGE_DOWN",
				"scrollDownChangeSelection", "HOME", "selectFirstColumn", "END",
				"selectLastColumn", "shift PAGE_UP", "scrollUpExtendSelection",
				"shift PAGE_DOWN", "scrollDownExtendSelection", "shift HOME",
				"selectFirstColumnExtendSelection", "shift END",
				"selectLastColumnExtendSelection", "TAB",
				"selectNextColumnCell", "shift TAB", "selectPreviousColumnCell",
				"meta A", "selectAll", "ESCAPE", "cancel", "ENTER",
				"selectNextRowCell", "shift ENTER", "selectPreviousRowCell",
				"alt TAB", "focusHeader", "alt shift TAB", "focusHeader"}));
	}

	LateBoundInputMap getTableRightToLeftInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"RIGHT",
				"selectPreviousColumn", "KP_RIGHT", "selectPreviousColumn",
				"LEFT", "selectNextColumn", "KP_LEFT", "selectNextColumn",
				"shift RIGHT", "selectPreviousColumnExtendSelection",
				"shift KP_RIGHT", "selectPreviousColumnExtendSelection",
				"shift LEFT", "selectNextColumnExtendSelection",
				"shift KP_LEFT", "selectNextColumnExtendSelection",
				"ctrl PAGE_UP", "scrollRightChangeSelection", "ctrl PAGE_DOWN",
				"scrollLeftChangeSelection", "ctrl shift PAGE_UP",
				"scrollRightExtendSelection", "ctrl shift PAGE_DOWN",
				"scrollLeftExtendSelection"}));
	}

	LateBoundInputMap getTreeInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"meta C",
				"copy", "meta V", "paste", "meta X", "cut", "COPY", "copy",
				"PASTE", "paste", "CUT", "cut", "UP", "selectPrevious", "KP_UP",
				"selectPrevious", "shift UP", "selectPreviousExtendSelection",
				"shift KP_UP", "selectPreviousExtendSelection", "DOWN",
				"selectNext", "KP_DOWN", "selectNext", "shift DOWN",
				"selectNextExtendSelection", "shift KP_DOWN",
				"selectNextExtendSelection", "RIGHT", "aquaExpandNode",
				"KP_RIGHT", "aquaExpandNode", "LEFT", "aquaCollapseNode",
				"KP_LEFT", "aquaCollapseNode", "shift RIGHT", "aquaExpandNode",
				"shift KP_RIGHT", "aquaExpandNode", "shift LEFT",
				"aquaCollapseNode", "shift KP_LEFT", "aquaCollapseNode",
				"ctrl LEFT", "aquaCollapseNode", "ctrl KP_LEFT",
				"aquaCollapseNode", "ctrl RIGHT", "aquaExpandNode",
				"ctrl KP_RIGHT", "aquaExpandNode", "alt RIGHT",
				"aquaFullyExpandNode", "alt KP_RIGHT", "aquaFullyExpandNode",
				"alt LEFT", "aquaFullyCollapseNode", "alt KP_LEFT",
				"aquaFullyCollapseNode", "meta A", "selectAll", "RETURN",
				"startEditing"}));
	}

	LateBoundInputMap getTreeRightToLeftInputMap()
	{
		return new LateBoundInputMap(new SimpleBinding(new String[]{"RIGHT",
				"aquaCollapseNode", "KP_RIGHT", "aquaCollapseNode", "LEFT",
				"aquaExpandNode", "KP_LEFT", "aquaExpandNode", "shift RIGHT",
				"aquaCollapseNode", "shift KP_RIGHT", "aquaCollapseNode",
				"shift LEFT", "aquaExpandNode", "shift KP_LEFT",
				"aquaExpandNode", "ctrl LEFT", "aquaExpandNode", "ctrl KP_LEFT",
				"aquaExpandNode", "ctrl RIGHT", "aquaCollapseNode",
				"ctrl KP_RIGHT", "aquaCollapseNode"}));
	}

	interface BindingsProvider
	{
		String[] getBindings();
	}

	static class SimpleBinding implements BindingsProvider
	{
		final String[] bindings;
		public SimpleBinding(final String[] bindings)
		{
			this.bindings = bindings;
		}
		public String[] getBindings()
		{
			return bindings;
		}
	}

	static class LateBoundInputMap implements LazyValue, BindingsProvider
	{
		private final BindingsProvider[] providerList;
		private String[] mergedBindings;

		public LateBoundInputMap(final BindingsProvider... providerList)
		{
			this.providerList = providerList;
		}

		public Object createValue(final UIDefaults table)
		{
			return LookAndFeel.makeInputMap(getBindings());
		}

		public String[] getBindings()
		{
			if (mergedBindings != null)
				return mergedBindings;

			final String[][] bindingsList = new String[providerList.length][];
			int size = 0;
			for (int i = 0; i < providerList.length; i++)
			{
				bindingsList[i] = providerList[i].getBindings();
				size += bindingsList[i].length;
			}

			if (bindingsList.length == 1)
			{
				return mergedBindings = bindingsList[0];
			}

			final ArrayList<String> unifiedList = new ArrayList<>(size);
			Collections.addAll(unifiedList, bindingsList[0]);

			for (int i = 1; i < providerList.length; i++)
			{
				mergeBindings(unifiedList, bindingsList[i]);
			}

			return mergedBindings = unifiedList
					.toArray(new String[unifiedList.size()]);
		}

		static void mergeBindings(final ArrayList<String> unifiedList,
				final String[] overrides)
		{
			for (int i = 0; i < overrides.length; i += 2)
			{
				final String key = overrides[i];
				final String value = overrides[i + 1];

				final int keyIndex = unifiedList.indexOf(key);
				if (keyIndex == -1)
				{
					unifiedList.add(key);
					unifiedList.add(value);
				} else
				{
					unifiedList.set(keyIndex, key);
					unifiedList.set(keyIndex + 1, value);
				}
			}
		}
	}

	void installAquaUpDownActions(final JTextComponent component)
	{
		final ActionMap actionMap = component.getActionMap();
		actionMap.put(upMultilineAction, moveUpMultilineAction);
		actionMap.put(downMultilineAction, moveDownMultilineAction);
		actionMap.put(pageUpMultiline, pageUpMultilineAction);
		actionMap.put(pageDownMultiline, pageDownMultilineAction);
	}
	abstract static class DeleteWordAction extends TextAction
	{
		public DeleteWordAction(final String name)
		{
			super(name);
		}

		public void actionPerformed(final ActionEvent e)
		{
			if (e == null)
				return;

			final JTextComponent target = getTextComponent(e);
			if (target == null)
				return;

			if (!target.isEditable() || !target.isEnabled())
			{
				UIManager.getLookAndFeel().provideErrorFeedback(target);
				return;
			}

			try
			{
				final int start = target.getSelectionStart();
				final Element line = Utilities.getParagraphElement(target,
						start);
				final int end = getEnd(target, line, start);

				final int offs = Math.min(start, end);
				final int len = Math.abs(end - start);
				if (offs >= 0)
				{
					target.getDocument().remove(offs, len);
					return;
				}
			} catch (final BadLocationException ignore)
			{
			}
			UIManager.getLookAndFeel().provideErrorFeedback(target);
		}

		abstract int getEnd(final JTextComponent target, final Element line,
				final int start) throws BadLocationException;
	}

	final TextAction moveUpMultilineAction = new AquaMultilineAction(
			upMultilineAction, DefaultEditorKit.upAction,
			DefaultEditorKit.beginAction);
	final TextAction moveDownMultilineAction = new AquaMultilineAction(
			downMultilineAction, DefaultEditorKit.downAction,
			DefaultEditorKit.endAction);
	final TextAction pageUpMultilineAction = new AquaMultilineAction(
			pageUpMultiline, DefaultEditorKit.pageUpAction,
			DefaultEditorKit.beginAction);
	final TextAction pageDownMultilineAction = new AquaMultilineAction(
			pageDownMultiline, DefaultEditorKit.pageDownAction,
			DefaultEditorKit.endAction);

	static class AquaMultilineAction extends TextAction
	{
		final String targetActionName;
		final String proxyActionName;

		public AquaMultilineAction(final String actionName,
				final String targetActionName, final String proxyActionName)
		{
			super(actionName);
			this.targetActionName = targetActionName;
			this.proxyActionName = proxyActionName;
		}

		public void actionPerformed(final ActionEvent e)
		{
			final JTextComponent c = getTextComponent(e);
			final ActionMap actionMap = c.getActionMap();
			final Action targetAction = actionMap.get(targetActionName);

			final int startPosition = c.getCaretPosition();
			targetAction.actionPerformed(e);
			if (startPosition != c.getCaretPosition())
				return;

			final Action proxyAction = actionMap.get(proxyActionName);
			proxyAction.actionPerformed(e);
		}
	}
}
